﻿# include"snake.h"

// 移动光标的位置
void SetPos(short x, short y)
{
	// 获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	// 定位光标的位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(houtput, pos); // 移动控制台光标到pos位置
}

// 打印界面
void WelcomeToGame()
{
	// 打印第一个界面
	SetPos(40, 13);
	wprintf(L"欢迎来到贪吃蛇游戏\n");
	SetPos(40, 25);
	system("pause"); // 暂停
	system("cls"); // 清除屏幕上的所有内容
	// 打印第二个画面
	SetPos(20, 13);
	wprintf(L"用W . A . S . D来控制蛇的移动。左Shift是加速，右Shift是减速\n");
	SetPos(38, 14);
	wprintf(L"食物的分数和蛇的速度相关\n");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

// 打印游戏地图(27行，58列) // 由于一个宽字符占两个字节 X轴方向打印29次就行 
void CreateMap()
{
	// 打印上边的墙
	for (int i = 0; i < 29; i++)
	{
		// 不用定位光标信息是因为默认就在左上角
		wprintf(L"%lc", WALL);
	}
	// 打印下边的墙
	SetPos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	// 打印左边的墙
	for (int i = 0; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	// 打印右边的墙
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);//  0~57  56-57 刚好是最后两个字节
		wprintf(L"%lc", WALL);
	}
}

// 初始化贪吃蛇
void InitSnake(pSnake ps)
{
	// 创建一个蛇
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));// 给蛇的每一个节点申请内存空间
		if (cur == NULL)
		{
			perror("InitSnake():malloc()");
			return;
		}
		cur->next = NULL;
		//给蛇的每一节身体初始化定位到地图里

		cur->x = POS_X + 2 * i; // 这个坐标必须是2的倍数 因为一个蛇身占屏幕的两个字节
		cur->y = POS_Y;

		//头插法将蛇的每一节链接起来
		if (ps->_pSnake == NULL) // 如果指向蛇头的指针是NULL 就把我们创建的cur给它
		{
			ps->_pSnake = cur;
		}
		else // 走到这里说明蛇头已经存在 ，使用头插法将节点链接起来
		{
			cur->next = ps->_pSnake; // 让新的cur蛇身节点的next指针指向原来的蛇头节点
			ps->_pSnake = cur; // 让蛇头更新 让cur成为新的蛇头
		}
	}
	// 这个时候的蛇已经被链接好了
	cur = ps->_pSnake; // 通过cur去遍历链表 将每个蛇的节点都通过图案打印出来
	while (cur)
	{
		SetPos(cur->x, cur->y);// 让光标定位到该节点的坐标
		// 将蛇的节点打印到光标处
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	// 设置贪吃蛇的属性
	// 由于我们的蛇头是放到最右边的，所以我们一开始的方向是向右走
	ps->_dir = RIGHT;//向右
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;
	ps->_status = OK;
}

// 创建食物
void CreateFood(pSnake ps)
{
	// [在墙体内且不要在蛇的身体内]随机生成食物 
	// [我们知道蛇的节点X坐标必须是2的倍数，同样的食物的X坐标也必须是2的倍数]
	// 因此我们可以推断出 x坐标的取值范围是 2~54   y的取值范围就是 1~25
	int x = 0;
	int y = 0;

again:
	// x = rand() % 53 + 2  x = 2~54 但是如果生成的随机值是奇数那就不行 x只能是2的倍数
	do 
	{
		x = rand() % 53 + 2;// x = 2 ~54  并且是2的倍数
		y = rand() % 25 + 1;// y = 1 ~25
	} while (x % 2 == 1);

	// x和y的坐标不能和蛇的身体坐标重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur) // 遍历蛇链表
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	// 走到这里，食物的坐标是没有问题得了 那么我们就 打印出食物
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建一个食物节点
	if (pFood == NULL)
	{
		perror("CreateFood():malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(pFood->x, pFood->y); // 定位食物位置
	wprintf(L"%lc", FOOD);
	
	ps->_pFood = pFood;
}

// 初始化游戏
void GameStart(pSnake ps)
{
	// 0.设置窗口大小并隐藏光标
	// 先设置窗口大小
	system("mode con cols=100 lines=31");// 设置窗口大小
	system("title 贪吃蛇");// 设置窗口标题

	// 隐藏光标
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); // 获得屏幕控制台的勺柄
	CONSOLE_CURSOR_INFO cursor_info = { 0 }; // 创建一个存储光标信息的结构体
	GetConsoleCursorInfo(houtput, &cursor_info); // 获取控制台的光标信息
	cursor_info.bVisible = false;// 隐藏控制台光标
	SetConsoleCursorInfo(houtput, &cursor_info);// 设置控制台光标状态

	// 1. 打印界面
	WelcomeToGame(); // 打印初始界面
	// 2. 绘制地图
	CreateMap();
	// 3. 创建蛇
	InitSnake(ps);
	// 4. 创建食物
	CreateFood(ps);	
}


// 运行游戏

void PrintHelpInfo()
{
	SetPos(62, 15);
	wprintf(L"%ls", L"不能撞到墙壁，也不能咬到自己");
	SetPos(62, 16);
	wprintf(L"%ls", L"用W . A . S . D来控制蛇的移动。");
	SetPos(62, 17);
	wprintf(L"%ls", L"左Shift是加速，右Shift是减速");
	SetPos(62, 18);
	wprintf(L"%ls", L"吃掉食物可以得分");
	SetPos(62, 19);
	wprintf(L"%ls", L"速度越快食物分数越高");
	SetPos(62, 20);
	wprintf(L"%ls", L"速度越慢食物分数越低");
	SetPos(62, 22);
	wprintf(L"%ls", L"ESC：退出游戏，space：暂停游戏");
	SetPos(62, 23);
	wprintf(L"%ls", L"再按一次space即可解除暂停");
}

void Pause()
{
	while (1)
	{
		Sleep(200); // Sleep的参数是以毫秒为单位的
		// 暂停，需要有条件来打破这个循环 让游戏继续
		if (KEY_PRESS(VK_SPACE)) // 再按一次空格就继续
		{
			break;
		}
	}
}

int NextIsFood(pSnakeNode pnext, pSnake ps)
{
	return (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y); // 也可以使用if语句
}

void EatFood(pSnakeNode pnext, pSnake ps)
{
	// 食物节点本质上和蛇的节点是同类型（SnakeNode）的 只是打印出来图案不一样而已
	// 因此我们可以采用头插法，直接将食物节点作为蛇节点头插到我们的蛇身上，再将下一个节点的空间释放掉
	ps->_pFood->next = ps->_pSnake; // 让食物节点的next指针指向蛇头
	ps->_pSnake = ps->_pFood;// 让食物节点成为新的蛇头  
	
	// 释放我们之前准备的下一个节点的空间 【如果没有食物，这个节点就会成为新的蛇头，并将蛇的尾节点删除】
	free(pnext);
	pnext = NULL; // 由于传入的是一级指针，因此并不会将pnext的实参置为NULL

	pSnakeNode cur = ps->_pSnake; // 让cur去指向蛇头
	// 遍历链表，打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y); 
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	// 走到这里 蛇打印完了 也就是走了一步了
	// 那我们要记分  吃下食物会加分
	ps->_score += ps->_food_weight; // 总分去加上吃掉一个食物的分数

	// 吃掉了食物，那我们就需要去重新创建一个食物出来
	CreateFood(ps); // 传入ps是因为要判断生成食物的坐标是否和蛇坐标重叠
}

void NoFood(pSnakeNode pnext, pSnake ps)
{
	//  下一步没有食物 那就直接将下一步的节点头插到蛇身来
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext; // 让下一步的节点成为新的蛇头

	pSnakeNode cur = ps->_pSnake;
	pSnakeNode prev = ps->_pSnake; // 让prev指向cur的前一个节点
	//遍历链表 找到蛇的尾节点 
	while (cur->next != NULL) //跳出循环说明cur指向的是尾节点
	{
		prev = cur;
		// 把尾节点之前的蛇身全部打印出来
		SetPos(prev->x, prev->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	// 此时cur指向尾节点，prev指向倒数第二个节点
	// 先把尾节点的图案用空格覆盖掉
	SetPos(cur->x, cur->y);
	printf("  ");
	
	// 释放尾节点
	free(cur);
	cur = NULL;

	prev->next = NULL; // 记得要将新的尾节点的next指针置为空 
}

void KillByWall(pSnake ps)
{
	// 只要检测蛇头的坐标是否和墙壁重叠就行
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56
		|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}

void KillBySelf(pSnake ps)
{
	// 检测蛇头的坐标是否和自己的身体重叠
	pSnakeNode cur = ps->_pSnake->next; // 让cur指向蛇的第二个节点
	// 遍历链表
	while (cur)
	{
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y) // x坐标和y坐标都重叠才是一定撞到自己了
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
	
}

// 蛇走一步的过程
void SnakeMove(pSnake ps)
{
	// 先创建一个节点，表示蛇即将走到的下一个节点
	pSnakeNode pNextnode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextnode == NULL)
	{
		perror("SnakeMove():malloc()");
		return;
	}

	// 既然要走，那得判断走的方向
	switch (ps->_dir)
	{
	// 根据方向来确定下一个节点的坐标
	case UP:  // 上
		pNextnode->x = ps->_pSnake->x;
		pNextnode->y = ps->_pSnake->y - 1;
		break;
	case DOWN: // 下
		pNextnode->x = ps->_pSnake->x;
		pNextnode->y = ps->_pSnake->y + 1;
		break;
	case LEFT:  // 左
		pNextnode->x = ps->_pSnake->x - 2; // 因为一个蛇身占2个字节 之前有说过
		pNextnode->y = ps->_pSnake->y;
		break;
	case RIGHT:  // 右
		pNextnode->x = ps->_pSnake->x + 2;
		pNextnode->y = ps->_pSnake->y;
		break;
	}
	// 将下一个节点的坐标算好后 ， 我们还需要判断 该坐标是否有食物生成了
	if (NextIsFood(pNextnode, ps)) // 检测下一步的坐标处是否有食物生成
	{
		// 如果是食物就直接吃掉 不需要删除蛇的尾节点
		EatFood(pNextnode, ps);
		pNextnode = NULL; // 不置为NULL 就是野指针了
	}
	else
	{
		// 如果不是食物，那就要删除蛇的尾节点
		NoFood(pNextnode, ps);
	}

	// 检测蛇是否撞墙
	KillByWall(ps);

	// 检测蛇是否撞到自己
	KillBySelf(ps);
}


// 游戏的运行逻辑
void GameRun(pSnake ps)
{
	PrintHelpInfo();// 打印帮助信息 【这个信息是无需变化的 打印一次即可】

	int countspeed = 5;
	do 
	{
		// 打印总分数和食物的分值
		SetPos(62, 8);
		printf("总分数：%d\n", ps->_score);
		SetPos(62, 9);
		printf("当前速度下食物的分数：%2d\n", ps->_food_weight);
		SetPos(62, 11);
		printf("当前速度档位：%2d\n", countspeed);
		SetPos(62, 12);
		printf("最高档位为10，最低为0");
		
		// 判断键盘的按键状态 从而选择方向
		if (KEY_PRESS(0x57) && ps->_dir != DOWN) // 0x57 是W  要注意如果按W 让蛇向上走，那么此时蛇不能向下走
		{
			ps->_dir = UP;// 将蛇的方向改成向上走
		}
		else if (KEY_PRESS(0x53) && ps->_dir != UP) // 0x53是S  要按S 此时蛇不能向上走
		{
			ps->_dir = DOWN; 
		}
		else if (KEY_PRESS(0x41) && ps->_dir != RIGHT) // 0x41是A
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(0x44) && ps->_dir != LEFT) // 0x44是D
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE)) // VK_SPACE 空格
		{
			// 暂停
			Pause();
		}
		else if (KEY_PRESS(VK_LSHIFT)) // VK_LSHIFT 左Shift 加速
		{
			// 加速
			if (ps->_sleep_time > 50) // 限制 加速 一共有5个档 
			{
				ps->_sleep_time -= 30; // 休息时间越长 那就越慢 休息时间越短 那就越快 
				ps->_food_weight += 2;
				countspeed++;
			}
		}
		else if (KEY_PRESS(VK_RSHIFT)) // VK_RSHIFT  右Shift 减速
		{
			//减速
			if (ps->_food_weight > 0) // 限制减速也有5个档
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
				countspeed--;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE)) // VK_ESCAPE ESC
		{
			// 正常退出游戏
			ps->_status = END_NORMAL;
		}

		// 检测完按键之后贪吃蛇需要走一步
		SnakeMove(ps); // 贪吃蛇走一步的过程

		Sleep(ps->_sleep_time); // 停顿ps->_sleep_time 毫秒

	} while (ps->_status == OK); // 如果蛇的状态是OK 那就继续
}


//  游戏结束后的善后工作
void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("您主动结束游戏\n");
		break;
	case KILL_BY_SELF:
		printf("蛇撞到了自己，游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("蛇撞到墙上，游戏结束\n");
		break;
	}

	// 游戏结束了  释放蛇链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}
	// 走到这里 链表已经释放完了
}